// Copyright (c) 2009 Paul Kohler
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
// and associated documentation files (the "Software"), to deal in the Software without restriction, 
// including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in all copies or substantial 
// portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FileSystemDatabase.Exceptions;
using Ionic.Zip;

namespace FileSystemDatabase
{
	/// <summary>
	/// A class to manage the reading, writing and deleting of serialized files to a
	/// directory on a file system.
	/// </summary>
	/// <remarks>
	/// <para>
	/// Probably the best way to use the <see cref="FileSystemDatabase"/> class is by a repository.
	/// See the <see cref="BaseJsonRepository"/> as an example.
	/// </para>
	/// </remarks>
	public abstract class FileSystemDatabase
	{
		protected static readonly object syncLock = new object();
		private readonly List<char> _badFilenameChars;

		/// <summary>
		/// Initializes a new instance of the <see cref="FileSystemDatabase"/> class
		/// Using the <paramref name="basePath"/> as the location of the file system database.
		/// </summary>
		/// <param name="basePath">The base path of the file system database.</param>
		protected FileSystemDatabase(string basePath)
		{
			BasePath = basePath;
			_badFilenameChars = new List<char>(Path.GetInvalidFileNameChars());
		}

		/// <summary>
		/// Gets or sets the base path.
		/// </summary>
		/// <value>The base path of the file system database.</value>
		/// <remarks>
		/// The process accessing the files must have read, write and delete permissions
		/// to the directory (depending on the required action).
		/// </remarks>
		public virtual string BasePath { get; private set; }

		/// <summary>
		/// Gets or sets a value indicating whether to store the files with indenting for readability.
		/// </summary>
		/// <value><c>true</c> if files are stored the files with indenting; otherwise, <c>false</c>.</value>
		public virtual bool WriteIndented { get; set; }

		/// <summary>
		/// Gets or sets the file extension for the current type of serialized file (e.g. "json" or "xml").
		/// </summary>
		/// <value>The file extension of the serialized files.</value>
		public virtual string FileExtension { get; set; }

		/// <summary>
		/// Writes the "<paramref name="row"/>" to disk.
		/// </summary>
		/// <typeparam name="T">An object that is serializable by the <see cref="Serialize"/> method.</typeparam>
		/// <param name="row">The item to write to disk.</param>
		/// <param name="id">The identifier of the item.</param>
		/// <returns>The full filename of the file.</returns>
		public virtual string Write<T>(T row, string id)
		{
			return Write(typeof(T), row, id);
		}

		/// <summary>
		/// Writes the "<paramref name="row"/>" to disk.
		/// </summary>
		/// <param name="rowType">Type of the <paramref name="row"/>.</param>
		/// <param name="row">The item to write to disk.</param>
		/// <param name="id">The identifier of the item.</param>
		/// <returns>The full filename of the file.</returns>
		public virtual string Write(Type rowType, object row, string id)
		{
			try
			{
				string filename = CreateFilename(rowType, id);
				string output = Serialize(row);

				lock (syncLock)
				{
					File.WriteAllText(filename, output);
				}
				return filename;
			}
			catch (Exception exp)
			{
				throw FileSystemDatabaseException.Create(ErrorType.Write, exp.Message, exp);
			}
		}

		/// <summary>
		/// Reads an object from disk by <typeparamref name="T"/> and <paramref name="id"/>.
		/// </summary>
		/// <typeparam name="T">The type to read.</typeparam>
		/// <param name="id">The identifier.</param>
		/// <returns>An instance of <typeparamref name="T"/> loaded from disk, or null if not found.</returns>
		public virtual T Read<T>(string id)
		{
			return (T) Read(typeof(T), id);
		}

		/// <summary>
		/// Reads an object from disk by <paramref name="rowType"/> and <paramref name="id"/>.
		/// </summary>
		/// <param name="rowType">Type of the row.</param>
		/// <param name="id">The identifier.</param>
		/// <returns>
		/// An instance of <paramref name="rowType"/> loaded from disk, or null if not found.
		/// </returns>
		public virtual object Read(Type rowType, string id)
		{
			try
			{
				string filename = CreateFilename(rowType, id);
				return InternalRead(rowType, filename);
			}
			catch (Exception exp)
			{
				throw FileSystemDatabaseException.Create(ErrorType.Read, exp.Message, exp);
			}
		}

		/// <summary>
		/// Performs the read action.
		/// </summary>
		/// <param name="rowType">Type of the object to load.</param>
		/// <param name="filename">The filename.</param>
		/// <returns></returns>
		protected virtual object InternalRead(Type rowType, string filename)
		{
			if (File.Exists(filename))
			{
				string fileData = File.ReadAllText(filename);
				return Deserialize(rowType, fileData);
			}
			return null;
		}

		/// <summary>
		/// Reads all files of type <typeparamref name="T"/> from the disk returning a collection.
		/// </summary>
		/// <typeparam name="T">The type of item to load.</typeparam>
		/// <returns>A collection of type <typeparamref name="T"/>.</returns>
		public virtual ICollection<T> Read<T>()
		{
			List<T> list = new List<T>();
			foreach (object obj in Read(typeof(T)))
			{
				list.Add((T)obj);
			}
			return list;
		}

		/// <summary>
		/// Reads all files of type "<paramref name="rowType"/>" from the disk returning a collection.
		/// </summary>
		/// <param name="rowType">Type of the object to load.</param>
		/// <returns>
		/// A collection of type "<paramref name="rowType"/>".
		/// </returns>
		public virtual ICollection Read(Type rowType)
		{
			try
			{
				string filePattern = string.Format("{0}-*.{1}", rowType.Name, FileExtension);
				string[] files = Directory.GetFiles(BasePath, filePattern, SearchOption.TopDirectoryOnly);
				List<object> list = new List<object>();
				foreach (string filename in files)
				{
					object obj = InternalRead(rowType, filename);
					if (obj != null)
					{
						list.Add(obj);
					}
				}
				return list;
			}
			catch (Exception exp)
			{
				throw FileSystemDatabaseException.Create(ErrorType.Read, exp.Message, exp);
			}
		}

		/// <summary>
		/// Deletes the object of type <typeparamref name="T"/> identified by the <paramref name="id"/>.
		/// </summary>
		/// <typeparam name="T">The type to delete.</typeparam>
		/// <param name="id">The identifier of the object.</param>
		public virtual void Delete<T>(string id)
		{
			Delete(typeof(T), id);
		}

		/// <summary>
		/// Deletes the object of type "<paramref name="type"/>" identified by the <paramref name="id"/>.
		/// </summary>
		/// <param name="type">The type to delete.</param>
		/// <param name="id">The identifier of the object.</param>
		public virtual void Delete(Type type, string id)
		{
			try
			{
				string filename = CreateFilename(type, id);
				if (File.Exists(filename))
				{
					File.Delete(filename);
				}
			}
			catch (Exception exp)
			{
				throw FileSystemDatabaseException.Create(ErrorType.Delete, exp.Message, exp);
			}
		}

		/// <summary>
		/// Creates a backup ZIP file of all the files with the specified <see cref="FileExtension"/>.
		/// The <paramref name="zipName"/> is placed in the <see cref="BasePath"/> directory.
		/// </summary>
		/// <param name="zipName">Name of the ZIP file.</param>
		/// <returns>The full path of the ZIP file.</returns>
		public virtual string CreateBackup(string zipName)
		{
			try
			{
				string zipFilename = Path.Combine(BasePath, zipName);

				using (ZipFile zip = new ZipFile())
				{
					lock (syncLock)
					{
						string filePattern = string.Format("*-*.{0}", FileExtension);
						string[] files = Directory.GetFiles(BasePath, filePattern, SearchOption.TopDirectoryOnly);
						foreach (string filename in files)
						{
							zip.AddFile(filename, string.Empty);
						}
						zip.Save(zipFilename);
					}

					return zipFilename;
				}
			}
			catch (Exception exp)
			{
				throw FileSystemDatabaseException.Create(ErrorType.Backup, exp.Message, exp);
			}
		}

		/// <summary>
		/// Creates a unique ID (a GUID string by default).
		/// </summary>
		/// <returns>A GUID string.</returns>
		public virtual string CreateID()
		{
			return Guid.NewGuid().ToString("D");
		}

		/// <summary>
		/// Creates the filename based on the <paramref name="type"/> and <paramref name="id"/>.
		/// </summary>
		/// <param name="type">The type of the object.</param>
		/// <param name="id">The identifier.</param>
		/// <returns>A filename, such as "Person-12345".</returns>
		protected virtual string CreateFilename(Type type, string id)
		{
			foreach (char ch in id)
			{
				if (_badFilenameChars.Contains(ch))
				{
					throw new InvalidOperationException(string.Format("The character '{0}' is not a valid object identifier.", ch));
				}
			}
			return Path.Combine(BasePath, string.Format("{0}-{1}.{2}", type.Name, id, FileExtension));
		}

		/// <summary>
		/// Serializes the specified object for writing to disk.
		/// </summary>
		/// <param name="row">The row.</param>
		/// <returns>A string representation of the object (e.g. JSON or XML)</returns>
		protected abstract string Serialize(object row);

		/// <summary>
		/// Deserializes the <paramref name="fileData"/> string into an object of type <paramref name="rowType"/>.
		/// </summary>
		/// <param name="rowType">Type of the row.</param>
		/// <param name="fileData">The file data (and XML or JSON string).</param>
		/// <returns>An instance of the object.</returns>
		protected abstract object Deserialize(Type rowType, string fileData);
	}
}